// @flow

import React, { PureComponent, type Node } from 'react';
import {
    Animated,
    Dimensions,
    TouchableWithoutFeedback,
    View
} from 'react-native';

import { type StyleType } from '../../../styles';

import styles from './slidingviewstyles';

/**
 * The type of the React {@code Component} props of {@link SlidingView}.
 */
type Props = {

    /**
     * The children of {@code SlidingView}.
     */
    children: Node,

    /**
     * Callback to notify the containing {@code Component} that the view is
     * closing.
     */
    onHide: Function,

    /**
     * Position of the SlidingView: 'left', 'right', 'top', 'bottom'.
     * later).
     */
    position: string,

    /**
     * Whether the {@code SlidingView} is to be displayed/rendered/shown or not.
     */
    show: boolean,

    /**
     * Style of the animated view.
     */
    style: StyleType
};

/**
 * The type of the React {@code Component} state of {@link SlidingView}.
 */
type State = {

    /**
     * Whether the sliding overlay should be displayed/rendered/shown.
     */
    showOverlay: boolean,

    /**
     * The native animation object.
     */
    sliderAnimation: Animated.Value,

    /**
     * Offset to move the view out of the screen.
     */
    positionOffset: number
};

/**
 * A generic animated slider view to be used for animated menus.
 */
export default class SlidingView extends PureComponent<Props, State> {
    /**
     * True if the component is mounted.
     */
    _mounted: boolean;

    /**
     * Implements React's {@link Component#getDerivedStateFromProps()}.
     *
     * @inheritdoc
     */
    static getDerivedStateFromProps(props: Props, prevState: State) {
        return {
            showOverlay: props.show || prevState.showOverlay
        };
    }

    /**
     * Initializes a new {@code SlidingView} instance.
     *
     * @inheritdoc
     */
    constructor(props: Props) {
        super(props);

        const { height, width } = Dimensions.get('window');
        const { position } = props;

        let positionOffset = height;

        if (position === 'left' || position === 'right') {
            positionOffset = width;
        }

        this.state = {
            showOverlay: false,
            sliderAnimation: new Animated.Value(0),
            positionOffset
        };

        // Bind event handlers so they are only bound once per instance.
        this._onHideMenu = this._onHideMenu.bind(this);
    }

    /**
     * Implements React's {@link Component#componentDidMount()}.
     *
     * @inheritdoc
     */
    componentDidMount() {
        this._mounted = true;
        this._setShow(this.props.show);
    }

    /**
     * Implements React's {@link Component#componentDidUpdate()}.
     *
     * @inheritdoc
     */
    componentDidUpdate() {
        this._setShow(this.props.show);
    }

    /**
     * Implements React's {@link Component#componentWillUnmount()}.
     *
     * @inheritdoc
     */
    componentWillUnmount() {
        this._mounted = false;
    }

    /**
     * Implements React's {@link Component#render()}.
     *
     * @inheritdoc
     */
    render() {
        const { showOverlay } = this.state;

        if (!showOverlay) {
            return null;
        }

        return (
            <View
                pointerEvents = 'box-none'
                style = { styles.sliderViewContainer } >
                <TouchableWithoutFeedback
                    onPress = { this._onHideMenu } >
                    <View style = { styles.sliderViewShadow } />
                </TouchableWithoutFeedback>
                <Animated.View
                    style = { this._getContentStyle() }>
                    { this.props.children }
                </Animated.View>
            </View>
        );
    }

    _getContentStyle: () => Array<Object>;

    /**
     * Assembles a style array for the SlideView content.
     *
     * @private
     * @returns {Array<Object>}
     */
    _getContentStyle() {
        const style = {
            ...this.props.style,
            ...styles.sliderViewContent
        };
        const { positionOffset } = this.state;

        switch (this.props.position) {
        case 'bottom':
            Object.assign(style, {
                bottom: -positionOffset,
                left: 0,
                right: 0,
                top: positionOffset
            }, {
                transform: [ { translateY: this.state.sliderAnimation } ]
            });
            break;
        case 'left':
            Object.assign(style, {
                bottom: 0,
                left: -positionOffset,
                right: positionOffset,
                top: 0
            }, {
                transform: [ { translateX: this.state.sliderAnimation } ]
            });
            break;
        }

        return style;
    }

    _onHideMenu: () => void;

    /**
     * Hides the slider.
     *
     * @private
     * @returns {void}
     */
    _onHideMenu() {
        this._setShow(false);

        const { onHide } = this.props;

        onHide && onHide();
    }

    _setShow: (boolean) => void;

    /**
     * Shows/hides the slider menu.
     *
     * @param {boolean} show - If the slider view is to be made visible,
     * {@code true}; otherwise, {@code false}.
     * @private
     * @returns {void}
     */
    _setShow(show) {
        if (!this._mounted) {
            return;
        }

        const { positionOffset } = this.state;
        const { position } = this.props;
        let toValue = positionOffset;

        if (position === 'bottom' || position === 'right') {
            toValue = -positionOffset;
        }

        Animated
            .timing(
                /* value */ this.state.sliderAnimation,
                /* config */ {
                    duration: 200,
                    toValue: show ? toValue : 0,
                    useNativeDriver: true
                })
            .start(({ finished }) => {
                finished && this._mounted && !show
                    && this.setState({ showOverlay: false });
            });
    }
}
